% This software is provided 'as-is', without any express or implied
% warranty.  In no event will the authors be held liable for any damages
% arising from the use of this software.
% Permission is granted to anyone to use this software for any purpose,
% including commercial applications, and to alter it and redistribute it
% freely

-module(erlyface).
-author("Jason Shaw <jasonshaw@gmail.com>").

-behaviour(gen_server).

-export([start_link/0,
		start/0,
		stop/0,
		init/1,
		handle_call/3,
		handle_cast/2,
		handle_info/2,
		code_change/3,
		terminate/2]).

-export([build_args_list/1,
		get_sig_md5/1,
		build_query_string/1,
		do_request/1,
		unix_timestamp/0,
		parse_json/1,
		call/2,
		friends_get/1,
		friends_getAppUsers/1,
		friends_getSquarePics/1,
		users_getInfo/1,
		friends_exclude_list/1,
		session_get/1,
		stream_get/1,
		stream_get/2,
		stream_getFilters/1,
		stream_getComments/1,
		get_pic_square/1,
		get_user_info/2,
		friends_getInfos/2]).

-define(API_SERVER, "api.facebook.com").
-define(API_KEY, "API_KEY").
-define(SECRET_KEY, "SECRET_KEY").
-define(TIMEOUT, 7500).

-include_lib("xmerl/include/xmerl.hrl").

start() ->
	start_link().
stop() ->
	gen_server:cast(?MODULE, stop).

start_link() ->
	gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
	io:format("starting ~p~n", [?MODULE]),
	inets:start(),
	{ok, {ok}}.

handle_cast(stop, State) ->
	{stop, normal, State};

handle_cast(_Msg, State) ->
	{noreply, State}.

handle_info(_Info, State) ->
	{noreply, State}.

terminate(Reason, State) ->
	io:format("~p terminateing ~p ~p~n", [?MODULE, Reason, State]),
	ok.

code_change(_Oldversion, State, _Extra) ->
	{ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% METHODS													%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Some convience methods
% get the friends of the user based on that user's uid
friends_get({uid, Uid}) ->
	gen_server:call(?MODULE, {"friends.get", [{"uid", Uid}]}, ?TIMEOUT);
% get the friends of a user based on the given session key
friends_get({session_key, Session_key}) ->
	gen_server:call(?MODULE, {"friends.get", [{"session_key", Session_key}]}, ?TIMEOUT).
% get the friends of a user based on the session key that have also added the app
friends_getAppUsers(Session_key) ->
	gen_server:call(?MODULE, {"friends.getAppUsers", [{"session_key", Session_key}]}).
% expecting a comma delimited list of uids or just a single uid
users_getInfo({"uids", Uid}) ->
	gen_server:call(?MODULE, {"users.getInfo", [{"uids", Uid}, {"fields", "uid,first_name,last_name,name,locale,affiliations,pic_square,profile_url"}]}).
% gets session from an auth token usually passed in from login.php
% returns {ok, {session_key, uid, expires}}
session_get(Auth_token) ->
	case gen_server:call(?MODULE, {"auth.getSession", [{"auth_token", Auth_token}]}) of
		{struct, [{<<"session_key">>, Key}, {<<"uid">>, Uid}, {<<"expires">>, Expires}]} ->
			{ok, {binary_to_list(Key), Uid, Expires}};
		Anything ->
			Anything
	end.
% gets the user's stream
stream_get(Session_key) ->
	stream_get(Session_key, []).
stream_get(Session_key, Options) ->
	gen_server:call(?MODULE, {"stream.get", [{"session_key", Session_key} | Options]}).
% gets filters associated with this user
stream_getFilters(Session_key) ->
	gen_server:call(?MODULE, {"stream.getFilters", [{"session_key", Session_key}]}).
% gets the comments for a post id
stream_getComments(Post) ->
	gen_server:call(?MODULE, {"stream.getComments", [{"post_id", Post}]}).
	
% these are some methods that I have found helpfull but aren't necessarily basic facebook calls
% get all the square pics of a user's friends
get_pic_square(Uid) ->
	[{struct, [{<<"uid">>, _}, {<<"pic_square">>, URL}]}] = gen_server:call(?MODULE, {"users.getInfo", [{"fields", "pic_square"}, {"uids", Uid}]}),
	binary_to_list(URL).
get_user_info(Uid, Fields) ->
	gen_server:call(?MODULE, {"users.getInfo", [{"fields", Fields}, {"uids", Uid}]}).
friends_getInfos(Session_key, Fields) ->
	Friends = friends_get({session_key, Session_key}),
	Friends2 = lists:foldl(fun(Elem, Accum) -> Accum ++ integer_to_list(Elem) ++ "," end, "", Friends),
	Friends3 = lists:sublist(Friends2, length(Friends2) - 1),
	gen_server:call(?MODULE, {"users.getInfo", [{"fields", Fields}, {"uids", Friends3}]}).
friends_getSquarePics(Session_key) ->
	Friends = friends_get({session_key, Session_key}),
	Friends2 = lists:foldl(fun(Elem, Accum) -> Accum ++ integer_to_list(Elem) ++ "," end, "", Friends),
	Friends3 = lists:sublist(Friends2, length(Friends2) - 1),
	gen_server:call(?MODULE, {"users.getInfo", [{"fields", "pic_square"}, {"uids", Friends3}]}).
% this returns a list of friends in a comma delimited list
% this be used as an exclude list on the invite friends to this app, that way the user isn't given a choice to 
% invite friends to use the app that already do so (which would be annoying)
friends_exclude_list(Session_key) ->
	case friends_get({session_key, Session_key}) of
		{struct, []} ->
			"";
		Friends_list ->
			lists:foldl(fun(Elem, Accum) -> integer_to_list(Elem) ++ "," ++ Accum end, "", Friends_list)
	end.
	

% Generic call to facebook
call(Method, Args) when is_list(Args) ->
	gen_server:call(?MODULE, {Method, Args});
call(_Method, _Args) ->
	{error, invalid_syntax}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% CALL BACK METHODS											%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

handle_call({Method, Args}, _From, State) ->
	Args_list = build_args_list([{"method", Method} | Args]),
	Query = build_query_string(Args_list),
	Return_data = do_request(Query),
	{reply, Return_data, State};
handle_call(_Request, _From, State) ->
	{ok, noreply, State}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% HELPER FUNCTIONS											%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% builds the default args list for facebook calls adds in the user's then sorts in alphbetical order
build_args_list(Args) ->
	Defaults = [{"api_key", ?API_KEY},
				{"call_id", integer_to_list(unix_timestamp())},
				{"format", "JSON"},
				{"v", "1.0"}],
	Arg_list = lists:foldl(fun(Elem, Accum) ->
		{Key, _} = Elem,
		case lists:keysearch(Key, 1, Accum) of
			false ->
				[Elem | Accum];
			_ ->
				Accum
		end
	end, Args, Defaults),
	lists:keysort(1, Arg_list).

% create the md5 hash from the arguments list
get_sig_md5(Args) ->
	Params = lists:map(
		fun({Key, Value}) ->
			[Key, "=", Value]
		end,
		Args
	),
	String = binary_to_list(erlang:md5(lists:flatten(Params, ?SECRET_KEY))),
	lists:flatten([io_lib:format("~2.16.0b", [X]) || X <- String]).
build_query_string(Args) ->
	Sig = get_sig_md5(Args),
	lists:flatten(["sig=", Sig, "&", mochiweb_util:urlencode(Args)]).
do_request(Data) ->
	Headers = [{"Content-length", integer_to_list(length(Data))},
			   {"Content-type", "application/x-www-form-urlencoded"},
			   {"Host", ?API_SERVER}],
	Content_type = "application/x-www-form-urlencoded",
	URL = lists:flatten(["http://", ?API_SERVER, "/restserver.php"]),
	Request = {URL, Headers, Content_type, Data},
	Http_options = [],
	Options = [{sync, true}, {headers_as_is, true}],
	Reply = http:request(post, Request, Http_options, Options),
	case Reply of
		{ok, {{_HttpVersion, 200, _ReasonPhrase}, _ResponseHeaders, ResponseBody }} ->
			parse_json(ResponseBody);
		{ok, {{_HttpVersion, _HttpCode, _ReasonPhrase}, _ResponseHeaders, ResponseBody }} ->
			{error, ResponseBody}
	end.

% need a unique increment id to give each call that goes to facebook
unix_timestamp() ->
	 calendar:datetime_to_gregorian_seconds(erlang:universaltime()).
% use mochi to parse the json response, wrap it in a try/catch block in case mochi
% throws a parse error
parse_json(Body) ->
	try mochijson2:decode(Body) of
		Response -> Response
	catch
		% sometimes facebook doesn't like to give back valid json data
		_:_ -> {error, Body}
	end.